package jeopardy.server.game;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import jeopardy.common.protocol.game.GameManagerCreateRequest;
import jeopardy.common.protocol.game.GameManagerCreateResponse;
import jeopardy.common.protocol.game.GameManagerJoinRequest;
import jeopardy.common.protocol.game.GameManagerJoinResponse;
import jeopardy.common.protocol.game.play.GameRecordMsg;
import jeopardy.server.ServerConsts;
import jeopardy.server.db.H2ServerDb;

import org.apache.log4j.Logger;

public class GameManager implements Runnable{

	private static Logger log = Logger.getLogger(GameManager.class);
	
	private ServerSocket ss;
	private H2ServerDb dbStore;
	
	private ConcurrentMap<String, Game> currentGames = new ConcurrentHashMap<String, Game>();
	
	public GameManager(ServerSocket ss, H2ServerDb dbStore){
		this.ss = ss;
		this.dbStore = dbStore;
	}
	
	public void reportFinished(GameRecordMsg gr){
		
		currentGames.remove(gr.getGid());
		dbStore.gameStoreFinishedGame(gr);
	}
	
	@Override
	public void run() {
		log.info("Starting Game Manager on " + ss.getLocalPort());
		
		ThreadPoolExecutor tpe = new ThreadPoolExecutor(
				ServerConsts.GM_CORETHREADS, ServerConsts.GM_POOLSIZE, 
				ServerConsts.GM_THREAD_KEEPALIVE_MILLI, TimeUnit.MILLISECONDS, 
				new LinkedBlockingQueue<Runnable>(ServerConsts.GM_MAX_WAITING_JOBS));
		
		while(ss.isBound() && !ss.isClosed()){
			Socket req = null;
			try{
				req = ss.accept();
				tpe.execute(new GameManagerServicer(req, this, dbStore));
			}catch(IOException ioe){
				log.info("Error accepting connection from client", ioe);
			}catch(RejectedExecutionException ree){
				log.info("Failed to accept more requests", ree);
				try{
					req.close();
				}catch(Exception e ){
					log.error("task rejected, and socket was not closed.  Turn on debug logging for more info");
					log.debug("task rejected, and socket was not closed.", e);
				}; 
			}catch(Exception e){
				log.error("Unknown exception.  Turn on debug logging for more info");
				log.debug("Error accepting/tasking new account request", e);
			}
		}
	}
	
	private class GameManagerServicer implements Runnable{
		private Socket s;
		private GameManager manager;
		private H2ServerDb db;
		private ObjectInputStream ois;
		private ObjectOutputStream oos;
		
		GameManagerServicer(Socket s, GameManager gm, H2ServerDb db) throws Exception{
			this.s = s;
			this.manager = gm;
			this.db = db;
			this.ois = new ObjectInputStream(s.getInputStream());
			this.oos = new ObjectOutputStream(s.getOutputStream());
			s.setSoTimeout(ServerConsts.GAME_CONN_TIMEOUT_MILLI);
		}
		
		@Override
		public void run(){
			try{
				s.setSoTimeout(ServerConsts.GM_CONN_TIMEOUT_MILLI);
				
				Object o = ois.readObject();
				if(o instanceof GameManagerJoinRequest){
					GameManagerJoinRequest req = (GameManagerJoinRequest) o;
					if(!db.authUser(req.getUser(), req.getPass())){
						oos.writeObject(new GameManagerJoinResponse(false, "cant authenticate"));
						return;
					}
					Client c = new Client(req.getUser(), s);
					Game g = currentGames.get(req.getGameId());
					if(g == null){
						oos.writeObject(new GameManagerJoinResponse(false, "couldn't join game"));
					}else{
						oos.writeObject(new GameManagerJoinResponse(true, g.getGid()));
						g.joinGame(c);
					}
					
				}else if(o instanceof GameManagerCreateRequest){
					GameManagerCreateRequest req = (GameManagerCreateRequest) o;
					if(!db.authUser(req.getUser(), req.getPass())){
						oos.writeObject(new GameManagerCreateResponse(false, "cant authenticate"));
						return;
					}
					if(currentGames.size() >= ServerConsts.MAX_CONCURRENT_GAMES){
						oos.writeObject(new GameManagerCreateResponse(false, "can't create"));
						return;
					}
					Client c = new Client(req.getUser(), s);
					int tries = 0;
					int MAX_TRIES = 5;
					boolean success = false;
					
					while(tries < MAX_TRIES && !success){
						String newId = UUID.randomUUID().toString();
						
						Game g = new Game(newId, manager, db, c.getUserName());
						if(currentGames.putIfAbsent(newId, g) != null){
							tries++;
						}else{
							dbStore.createGame(g.getGid(), c.getUserName());
							success = true;
							oos.writeObject(new GameManagerCreateResponse(true, g.getGid()));
							g.joinGame(c);
							Thread t = new Thread(g);
							t.start();
						}
					}
					if(!success){
						oos.writeObject(new GameManagerCreateResponse(false, "can't create game"));
					}
				}
			}catch(Exception e){
				log.info("Error servicing game manager request");
				log.debug("Error servicing game manager request", e);
				try{
					if(s != null && !s.isClosed()){
						s.close();
					}
				}catch(Exception ee){
					log.error("failed to close socket on game manager request. turn on debug logging for more info");
					log.debug("failed to close socket on game manager request.", ee);
				}
			}
		}
	}

	public void reportStart(String gid, List<String> initialClients) {
		dbStore.startGame(gid, initialClients);
	}
}